package de.justphil.tcg.tcgserver.rest.services;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.slf4j.Logger;

import de.justphil.tcg.tcgserver.commons.config.Config;
import de.justphil.tcg.tcgserver.commons.domain.containers.GameExpired;
import de.justphil.tcg.tcgserver.commons.domain.containers.GameOver;
import de.justphil.tcg.tcgserver.commons.domain.containers.ObjectHeader;
import de.justphil.tcg.tcgserver.commons.domain.containers.ObjectHeader.Type;
import de.justphil.tcg.tcgserver.commons.exceptions.TCGException;
import de.justphil.tcg.tcgserver.commons.util.AtomicIdGenerator;
import de.justphil.tcg.tcgserver.rest.resources.GameResource;

import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;

public class GameService extends Thread {
	private static final Logger log = restLogger(GameService.class);
	
	private static final GameService instance = new GameService(
			Config.WEBSOCKET_SERVER_HOSTNAME,
			Config.WEBSOCKET_SERVER_TCGAME_CREATOR_PORT,
			Config.WEBSOCKET_SERVER_TCGAME_CREATOR_SOCKET_TIMEOUT
	);
	
	private final ConcurrentMap<Long, GameServiceSynchronizer> synchronizers;
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	
	private volatile boolean stopped;
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	private volatile Socket socket;
	
	private volatile ObjectInputStream oin;
	
	private volatile ObjectOutputStream oout;
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	private final String host;
	
	private final int port, timeout;
	
	
	private GameService(String host, int port, int timeout) {
		super(GameService.class.getSimpleName() + "-Thread");
		
		this.synchronizers = new ConcurrentHashMap<Long, GameServiceSynchronizer>();
		this.host = host;
		this.port = port;
		this.timeout = timeout;
	}
	
	public static GameService getInstance() {
		return instance;
	}

	@Override
	public synchronized void start() {
		stopped = false;
		super.start();
	}

	@Override
	public void run() {
		while (!stopped) {
			
			try {
				connect();
				initStreams();
			}
			catch (TCGException e) {
				//log.info("TCGException while connecting to WebSocket server: {}", e.getCause().getMessage());
				continue;
			}
			
			while (!Thread.interrupted()) {
				try {
					// Blocking call
					handleReceivedObjectHeader((ObjectHeader) oin.readObject());
				} catch (InterruptedIOException e) {
					log.debug("InterruptedIOException during readObject()!");
					break;
				} catch (IOException e) {
					log.error("IOException during readObject()!");
					break;
				} catch (ClassNotFoundException e) {
					log.error("ClassNotFoundException during readObject()!");
					continue;
				}
			}
			
			cleanUp();
		}
	}
	
	public void stopIt() {
		log.debug("stopIt()");
		
		if (stopped) {
			return;
		}
		
		stopped = true;
		
		if (this.isAlive() && !this.isInterrupted()) {
			this.interrupt();
		}
	}
	
	public boolean sendObject(Object obj) {
		log.debug("sendObject()");
		
		if (stopped) {
			log.debug("GameService has been already stopped!");
			return false;
		}
		
		long requestId = AtomicIdGenerator.nextId();
		GameServiceSynchronizer synchronizer = new GameServiceSynchronizer();
		synchronizers.put(requestId, synchronizer);
		
		synchronized (this) {
			try {
				log.debug("Sending object to WebSocket server...");
				
				oout.writeObject( new ObjectHeader(obj.getClass().getSimpleName(), requestId, Type.REQUEST) );
				oout.writeObject(obj);
				oout.flush();
				
				log.debug("Sending object to WebSocket server...DONE!");
			} catch (IOException e) {
				log.error("IOException during writeObject()!");
				
				synchronizers.remove(requestId);
				
				return false;
			}
		}
		
		// Let the calling thread block until the GameService-Thread gets the response.
		synchronized (synchronizer) {
			while (synchronizers.get(requestId).getResponse() == null) {
				try {
					log.debug("Waiting for response...");
					synchronizer.wait();
				} catch (InterruptedException ignored) {
					log.debug("Interrupt or spurious wakeup by the JVM!");
				}
			}
			
			log.debug("Waiting for response...READY!");
		}
		
		// Remove synchronizer object
		synchronizer = synchronizers.remove(requestId);
		
		// TODO: Remove debug code.
		if (synchronizer == null) {
			log.debug("synchronizer is null.");
		}
		else {
			log.debug("synchronizer is NOT null.");
		}
		
		if (synchronizer.getResponse() == null) {
			log.debug("synchronizer.getResponse() is null.");
		}
		else {
			log.debug("synchronizer.getResponse() is NOT null.");
		}
		
		return synchronizer.getResponse().isSuccessful();
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	private void handleReceivedObjectHeader(ObjectHeader oh) throws IOException, ClassNotFoundException {
		log.debug("handleReceivedObjectHeader() ## type => {} ## requestId => {}", oh.getType(), oh.getRequestId());
		
		if (oh.getType() == Type.RESPONSE) {
			GameServiceSynchronizer synchronizer = synchronizers.get(oh.getRequestId());
			
			if (synchronizer != null) {
				synchronizer.setResponse( new GameServiceResponse( true ) );
				
				synchronized (synchronizer) {
					log.debug("Notifying synchronizer about received response...");
					synchronizer.notifyAll();
					log.debug("Notifying synchronizer about received response...DONE!");
				}
			}
		}
		else if (oh.getType() == Type.REQUEST) {
			if (oh.getClassName().equals(ObjectHeader.GAME_EXPIRED)) {
				log.debug("ObjectHeader.GAME_EXPIRED");
				new GameResource().onGameExpired( (GameExpired) oin.readObject() );
			}
			else if (oh.getClassName().equals(ObjectHeader.GAME_OVER)) {
				log.debug("ObjectHeader.GAME_OVER");
				new GameResource().onGameOver( (GameOver) oin.readObject() );
			}
			else {
				assert false : "Unsupported ObjectHeader class name!";
			}
		}
		else {
			assert false : "Unsupported ObjectHeader type!";
		}
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	private void connect() throws TCGException {
		//log.debug("connect()");
		
		try {
			SocketAddress sockAddr = new InetSocketAddress(host, port);
			socket = new Socket();
			socket.connect(sockAddr, timeout);
			
			log.info("Connected to {} on port {}!", host, port);
		}
		catch (SocketTimeoutException e) {
			throw new TCGException(e);
		}
		catch (IOException e) {
			throw new TCGException(e);
		}
	}
	
	private void initStreams() throws TCGException {
		log.debug("initStreams()");
		
		try {
			log.info("Initializing streams...");
			
			
			// First, init the ObjectOutputStream, afterwards init the ObjectInputStream!!!
			// Reason: http://www.jguru.com/faq/view.jsp?EID=333392
			log.info("Initializing ObjectOutputStream...");
			oout	= new ObjectOutputStream(socket.getOutputStream());
			/*
			 * JavaDoc 6 SE says:
			 * "[...] callers may wish to flush the stream immediately to ensure that constructors
			 * for receiving ObjectInputStreams will not block when reading the header [...]"
			 */
			log.info("Writing serialization stream header to the underlying stream...");
			oout.flush();
			
			log.info("Initializing ObjectInputStream...");
			oin		= new ObjectInputStream(socket.getInputStream());
			
			log.info("Initializing streams...DONE!");
		}
		catch (IOException e) {
			throw new TCGException(e);
		}
	}
	
	private void cleanUp() {
		log.debug("cleanUp()");
		
		try {
			if (oin != null) {
				oin.close();
			}
			
			if (oout != null) {
				oout.close();
			}
			
			if (socket != null) {
				socket.close();
			}
			
			clearSynchronizers();
		}
		catch (IOException e) {
			log.warn("IOException during cleanUp(): {}", e.getMessage());
		}
	}
	
	private void clearSynchronizers() {
		GameServiceSynchronizer synchronizer = null;
		for (Long requestId : synchronizers.keySet()) {
			synchronizer = synchronizers.get(requestId);
			synchronizer.setResponse( new GameServiceResponse( false ) );
			synchronized (synchronizer) {
				synchronizer.notifyAll();
			}
		}
		
		synchronizers.clear();
	}
}
